package com.fqy.ticket.business.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fqy.ticket.business.domain.SkToken;
import com.fqy.ticket.business.domain.SkTokenExample;
import com.fqy.ticket.business.enums.RedisKeyPreEnum;
import com.fqy.ticket.business.mapper.SkTokenMapper;
import com.fqy.ticket.business.mapper.SkTokenMapperCust;
import com.fqy.ticket.business.req.SkTokenQueryReq;
import com.fqy.ticket.business.req.SkTokenSaveReq;
import com.fqy.ticket.business.resp.SkTokenQueryResp;
import com.fqy.ticket.common.resp.PageResp;
import com.fqy.ticket.common.util.SnowUtil;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class SkTokenService {

    private static final Logger LOG = LoggerFactory.getLogger(SkTokenService.class);

    @Resource
    private SkTokenMapper skTokenMapper;

    @Resource
    private DailyTrainSeatService dailyTrainSeatService;

    @Resource
    private DailyTrainStationService dailyTrainStationService;
    @Resource
    private SkTokenMapperCust skTokenMapperCust;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private final RedisScript<Long> acquireTokenScript;
    private final RedisScript<Long> initTokenScript;

    public SkTokenService() {
        acquireTokenScript = new DefaultRedisScript<>();
        ((DefaultRedisScript<Long>) acquireTokenScript).setScriptSource(
                new ResourceScriptSource(new ClassPathResource("scripts/acquire_token.lua"))
        );
        ((DefaultRedisScript<Long>) acquireTokenScript).setResultType(Long.class);

        initTokenScript = new DefaultRedisScript<>();
        ((DefaultRedisScript<Long>) initTokenScript).setScriptSource(
                new ResourceScriptSource(new ClassPathResource("scripts/init_token.lua")));
        ((DefaultRedisScript<Long>) initTokenScript).setResultType(Long.class);
    }

    // @Resource
    // private RedissonClient redissonClient;

    /**
     * 初始化
     */
    public void genDaily(Date date, String trainCode) {
        LOG.info("删除日期【{}】车次【{}】的令牌记录", DateUtil.formatDate(date), trainCode);
        SkTokenExample skTokenExample = new SkTokenExample();
        skTokenExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);
        skTokenMapper.deleteByExample(skTokenExample);

        DateTime now = DateTime.now();
        SkToken skToken = new SkToken();
        skToken.setDate(date);
        skToken.setTrainCode(trainCode);
        skToken.setId(SnowUtil.getSnowflakeNextId());
        skToken.setCreateTime(now);
        skToken.setUpdateTime(now);

        int seatCount = dailyTrainSeatService.countSeat(date, trainCode);
        LOG.info("车次【{}】座位数：{}", trainCode, seatCount);

        long stationCount = dailyTrainStationService.countByTrainCode(date, trainCode);
        LOG.info("车次【{}】到站数：{}", trainCode, stationCount);

        // 3/4需要根据实际卖票比例来定，一趟火车最多可以卖（seatCount * stationCount）张火车票
        int count = (int) (seatCount * (stationCount-1));
        LOG.info("车次【{}】初始生成令牌数：{}", trainCode, count);
        skToken.setCount(count);

        skTokenMapper.insert(skToken);
    }

    /**
     * 保存SkToken记录（新增或更新）
     * @param req 保存请求对象
     */
    public void save(SkTokenSaveReq req) {
        DateTime now = DateTime.now();
        SkToken skToken = BeanUtil.copyProperties(req, SkToken.class);

        if (ObjectUtil.isNull(skToken.getId())) {
            // 新增逻辑
            skToken.setId(SnowUtil.getSnowflakeNextId());
            skToken.setCreateTime(now);
            skToken.setUpdateTime(now);
            skTokenMapper.insert(skToken);
            LOG.info("新增SkToken成功，ID：{}", skToken.getId());
        } else {
            // 更新逻辑
            skToken.setUpdateTime(now);
            skTokenMapper.updateByPrimaryKey(skToken);
        }
    }

    /**
     * 分页查询SkToken列表
     * @param req 查询请求对象（包含分页信息）
     * @return 分页结果
     */
    public PageResp<SkTokenQueryResp> queryList(SkTokenQueryReq req) {
        SkTokenExample skTokenExample = new SkTokenExample();
        skTokenExample.setOrderByClause("id desc");
        SkTokenExample.Criteria criteria = skTokenExample.createCriteria();

        LOG.info("查询SkToken列表，页码：{}，每页条数：{}", req.getPage(), req.getSize());
        PageHelper.startPage(req.getPage(), req.getSize());
        List<SkToken> skTokenList = skTokenMapper.selectByExample(skTokenExample);

        PageInfo<SkToken> pageInfo = new PageInfo<>(skTokenList);
        LOG.info("查询结果：总行数={}，总页数={}", pageInfo.getTotal(), pageInfo.getPages());

        List<SkTokenQueryResp> list = BeanUtil.copyToList(skTokenList, SkTokenQueryResp.class);

        PageResp<SkTokenQueryResp> pageResp = new PageResp<>();
        pageResp.setTotal(pageInfo.getTotal());
        pageResp.setList(list);
        return pageResp;
    }

    /**
     * 根据ID删除SkToken记录
     * @param id 记录ID
     */
    public void delete(Long id) {
        int count = skTokenMapper.deleteByPrimaryKey(id);
    }

    /**
     * 获取令牌
     */
    public boolean validSkToken(Date date, String trainCode, Long memberId) {
        LOG.info("会员【{}】获取日期【{}】车次【{}】的令牌开始", memberId, DateUtil.formatDate(date), trainCode);

        // 先获取令牌锁，再校验令牌余量，防止机器人抢票，lockKey就是令牌，用来表示【谁能做什么】的一个凭证
        // String lockKey = DateUtil.formatDate(date) + "-" + trainCode + "-" + memberId;
        String skTokenLockKey = RedisKeyPreEnum.SK_TOKEN + ":" + DateUtil.formatDate(date) + ":" + trainCode + ":" + memberId;

        // 尝试获取令牌锁，同一用户5秒内只能获取一次
        Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(skTokenLockKey, skTokenLockKey, 5, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(setIfAbsent)) {
            LOG.info("恭喜，抢到令牌锁了！lockKey：{}", skTokenLockKey);
        } else {
            LOG.info("很遗憾，没抢到令牌锁！lockKey：{}", skTokenLockKey);
            return false;
        }

        // 检查令牌余量
        String skTokenCountKey = RedisKeyPreEnum.SK_TOKEN_COUNT + ":" + DateUtil.formatDate(date) + ":" + trainCode;
        // 尝试扣减令牌余量缓存
        Long result = stringRedisTemplate.execute(
                acquireTokenScript,
                Collections.singletonList(skTokenCountKey),
                "1", // 减少数量
                "60" // 过期时间(秒)
        );

        // 令牌余量已经写到缓存了
        if (result!=null) {
            if (result < 0) {
                LOG.error("令牌数量不足，获取失败：{}", skTokenCountKey);
                return false;
            }
            LOG.info("成功获取令牌，剩余数量：{}", result);
            // 检查是否需要更新数据库
            processDatabaseUpdate(date, trainCode, result);
            return true;
        }

        // 令牌余量还没读到缓存，需要去读数据库
        SkTokenExample skTokenExample = new SkTokenExample();
        skTokenExample.createCriteria()
                .andDateEqualTo(date)
                .andTrainCodeEqualTo(trainCode);
        // 车次不存在
        List<SkToken> tokenList = skTokenMapper.selectByExample(skTokenExample);
        if (CollUtil.isEmpty(tokenList)) {
            LOG.info("未找到令牌记录，日期：{}，车次：{}",
                    DateUtil.formatDate(date), trainCode);
            return false;
        }
        // 数据库余量已经为0，不能获取。
        SkToken skToken = tokenList.get(0);
        if (skToken.getCount() <= 0) {
            LOG.info("令牌余量为0，设置空缓存，日期：{}，车次：{}", DateUtil.formatDate(date), trainCode);

            // 缓存空值（设置为0，有效期5分钟）
            stringRedisTemplate.opsForValue().set(
                    skTokenCountKey,
                    "0",
                    60,
                    TimeUnit.SECONDS
            );
            return false;
            // LOG.info("令牌余量为0，日期：{}，车次：{}",
            //         DateUtil.formatDate(date), trainCode);
            // return false;
        }

        // 3. 原子性初始化令牌并获取，即使多个线程进入也只能有一个线程更新成功缓存
        Integer initialCount = skToken.getCount();
        result = stringRedisTemplate.execute(
                initTokenScript,
                Collections.singletonList(skTokenCountKey),
                String.valueOf(initialCount),
                "60"
        );

        if (result == null || result <= 0) {
            LOG.error("初始化后获取令牌失败，日期：{}，车次：{}",
                    DateUtil.formatDate(date), trainCode);
            return false;
        }

        LOG.info("初始化后成功获取令牌，剩余数量：{}", result);
        processDatabaseUpdate(date, trainCode, result);
        return true;


//         String skTokenCountKey = RedisKeyPreEnum.SK_TOKEN_COUNT + ":" + DateUtil.formatDate(date) + ":" + trainCode;
//         Object skTokenCount = stringRedisTemplate.opsForValue().get(skTokenCountKey);
//         if (skTokenCount != null) {
//             LOG.info("缓存中有该车次令牌大闸的key：{}", skTokenCountKey);
//             Long count = stringRedisTemplate.opsForValue().decrement(skTokenCountKey, 1);
//             stringRedisTemplate.expire(skTokenCountKey, 60, TimeUnit.SECONDS);
//             if (count < 0L) {
//                 LOG.error("获取令牌失败：{}", skTokenCountKey);
//                 return false;
//             } else {
//                 LOG.info("获取令牌后，令牌余数：{}", count);
//                 // 每获取5个令牌更新一次数据库
//                 if (count % 5 == 0) {
//                     skTokenMapperCust.decrease(date, trainCode, 5);
//                 }
//                 return true;
//             }
//         } else {
//             LOG.info("缓存中没有该车次令牌大闸的key：{}", skTokenCountKey);
//             // 检查是否还有令牌
//             SkTokenExample skTokenExample = new SkTokenExample();
//             skTokenExample.createCriteria().andDateEqualTo(date).andTrainCodeEqualTo(trainCode);
//             List<SkToken> tokenCountList = skTokenMapper.selectByExample(skTokenExample);
//             if (CollUtil.isEmpty(tokenCountList)) {
//                 LOG.info("找不到日期【{}】车次【{}】的令牌记录", DateUtil.formatDate(date), trainCode);
//                 return false;
//             }
//
//             SkToken skToken = tokenCountList.get(0);
//             if (skToken.getCount() <= 0) {
//                 LOG.info("日期【{}】车次【{}】的令牌余量为0", DateUtil.formatDate(date), trainCode);
//                 return false;
//             }
//
//             // 令牌还有余量
//             // 令牌余数-1
//             Integer count = skToken.getCount() - 1;
//             skToken.setCount(count);
//             LOG.info("将该车次令牌大闸放入缓存中，key: {}， count: {}", skTokenCountKey, count);
//             // 不需要更新数据库，只要放缓存即可
//             stringRedisTemplate.opsForValue().set(skTokenCountKey, String.valueOf(count), 60, TimeUnit.SECONDS);
// //            skTokenMapper.updateByPrimaryKey(skToken);
//             return true;
//         }
        // 令牌约等于库存，令牌没有了，就不再卖票，不需要再进入购票主流程去判断库存，判断令牌肯定比判断库存效率高
        // int updateCount = skTokenMapperCust.decrease(date, trainCode, 1);
        // return updateCount > 0;
    }

    // 处理数据库更新（带分布式锁保护）
    private void processDatabaseUpdate(Date date, String trainCode, Long count) {
        if (count % 5 != 0) {
            LOG.info("缓存余量{}不整除5，不更新", count);
            return;
        }

        LOG.info("开始更新数据库，日期：{}，车次：{}",
                DateUtil.formatDate(date), trainCode);

        String lockKey = RedisKeyPreEnum.SK_TOKEN_MYSQL_UPDATE + ":" +
                DateUtil.formatDate(date) + ":" + trainCode;

        // 可以改成redission的锁，有看门狗机制防止锁过期
        // 获取分布式锁（10秒过期）
        Boolean dbLocked = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, "lockKey", 10, TimeUnit.SECONDS);

        if (dbLocked != null && dbLocked) {
            try {
                // 数据库更新使用乐观锁
                int affectedRows = skTokenMapperCust.decrease(
                        date, trainCode, 5
                );
                if (affectedRows == 0) {
                    LOG.warn("数据库更新失败，令牌可能已被其他线程耗尽");
                }
            } finally {
                stringRedisTemplate.delete(lockKey);
            }
        }
    }
}